﻿using System;
using System.Runtime.InteropServices;

namespace Pfz.Drawing.Wpf
{
    /// <summary>
    /// Structure that holds a color in Argb (Bgr32) format.
    /// </summary>
	[Serializable]
    [StructLayout(LayoutKind.Explicit)]
    public struct Argb:
        IEquatable<Argb>,
        IComparable<Argb>,
        IComparable
    {
        /// <summary>
        /// Creates a new Argb setting all its components.
        /// </summary>
        public Argb(byte alpha, byte red, byte green, byte blue)
        {
            _alpha = alpha;
            _red = red;
            _green = green;
            _blue = blue;
        }

        /// <summary>
        /// Creates a new Argb setting only red, green and blue values. The Alpha value is set to 255.
        /// </summary>
        public Argb(byte red, byte green, byte blue)
        {
            _alpha = 255;
            _red = red;
            _green = green;
            _blue = blue;
        }

        [FieldOffset(3)]
        private readonly byte _alpha;

        /// <summary>
        /// Gets the Alpha (opacity) value.
        /// </summary>
        public byte Alpha
        {
            get
            {
                return _alpha;
            }
        }

        [FieldOffset(2)]
        private readonly byte _red;

        /// <summary>
        /// Gets the Red color element.
        /// </summary>
        public byte Red
        {
            get
            {
                return _red;
            }
        }

        [FieldOffset(1)]
        private readonly byte _green;

        /// <summary>
        /// Gets the Green color element.
        /// </summary>
        public byte Green
        {
            get
            {
                return _green;
            }
        }

        [FieldOffset(0)]
        private readonly byte _blue;

        /// <summary>
        /// Gets the Blue color element.
        /// </summary>
        public byte Blue
        {
            get
            {
                return _blue;
            }
        }

        /// <summary>
        /// Gets the combination of this color with an "old color".
        /// This will only work if Alpha is different than 255. If not, the actual color will be returned.
        /// </summary>
        public Argb CombineOver(Argb oldColor)
        {
            byte sourceAlpha = _alpha;
            if (sourceAlpha == 255)
                return this;

            int remainingAlpha = 255 - sourceAlpha;

            byte oldAlpha = oldColor._alpha;
            byte combinedAlpha = 255;

            if (oldAlpha != 255)
            {
                remainingAlpha = (remainingAlpha * oldAlpha) / 255;
                combinedAlpha = (byte)(sourceAlpha + remainingAlpha);
                sourceAlpha = (byte)(255 - remainingAlpha);
            }

            int red = ((_red * sourceAlpha) + (oldColor._red * remainingAlpha)) / 255;
            int green = ((_green * sourceAlpha) + (oldColor._green * remainingAlpha)) / 255;
            int blue = ((_blue * sourceAlpha) + (oldColor._blue * remainingAlpha)) / 255;
            Argb combinedColor = new Argb(combinedAlpha, (byte)red, (byte)green, (byte)blue);
            return combinedColor;
        }

        /// <summary>
        /// Combines this color with another color.
        /// The highest alpha is kept.
        /// </summary>
        public Argb CombineIn(Argb oldColor)
        {
            byte sourceAlpha = _alpha;
            if (sourceAlpha == 255)
                return this;

            int remainingAlpha = 255 - sourceAlpha;
            int red = ((_red * sourceAlpha) + (oldColor._red * remainingAlpha)) / 255;
            int green = ((_green * sourceAlpha) + (oldColor._green * remainingAlpha)) / 255;
            int blue = ((_blue * sourceAlpha) + (oldColor._blue * remainingAlpha)) / 255;
            Argb combinedColor = new Argb(oldColor._alpha, (byte)red, (byte)green, (byte)blue);
            return combinedColor;
        }

        /// <summary>
        /// Combines this color with an old color using the given combineMode.
        /// </summary>
        public Argb Combine(Argb oldColor, ColorCombineMode combineMode)
        {
            switch (combineMode)
            {
                case ColorCombineMode.Replace: return this;
                case ColorCombineMode.CombineIn: return CombineIn(oldColor);
                case ColorCombineMode.CombineOver: return CombineOver(oldColor);
            }

            throw new ArgumentException("Unknown value for combineMode.", "combineMode");
        }

        /// <summary>
        /// Gets the HashCode of this color.
        /// </summary>
        public override int GetHashCode()
        {
            return (_alpha << 24) | (_red << 16) | (_green << 8) | _blue;
        }

        /// <summary>
        /// Compares this Argb color with another object.
        /// </summary>
        public override bool Equals(object obj)
        {
            if (obj is Argb)
            {
                Argb other = (Argb)obj;
                return Equals(other);
            }

            return false;
        }

        /// <summary>
        /// Compares this Argb color with another one.
        /// </summary>
        public bool Equals(Argb other)
        {
            return _alpha == other._alpha && _red == other._red && _green == other._green && _blue == other._blue;
        }

        /// <summary>
        /// Compares two Argb instances for Equality.
        /// </summary>
        public static bool operator == (Argb a, Argb b)
        {
            return a.Equals(b);
        }

        /// <summary>
        /// Compares two Argb instances for Inequality.
        /// </summary>
        public static bool operator !=(Argb a, Argb b)
        {
            return !a.Equals(b);
        }

        private int _ApplyDifference(int oldDifference, int firstValue, int secondValue)
        {
            int newDifference = firstValue - secondValue;
            if (oldDifference == 0)
                return newDifference;

            if (oldDifference < 0)
                return oldDifference - Math.Abs(newDifference);

            return oldDifference + Math.Abs(newDifference);
        }

        /// <summary>
        /// Compares two colors and returns their numeric difference.
        /// </summary>
        public int CompareTo(Argb other)
        {
            int difference = 0;

            difference = _alpha - other._alpha;
            difference = _ApplyDifference(difference, _red, other._red);
            difference = _ApplyDifference(difference, _green, other._green);
            difference = _ApplyDifference(difference, _blue, other._blue);

            return difference;
        }

        #region IComparable Members
            int IComparable.CompareTo(object obj)
            {
                return CompareTo((Argb)obj);
            }
        #endregion
    }
}
